﻿using System;
using System.Runtime.CompilerServices;
using MemoryPack;

namespace Hsenl {
#if !UNITY_5_3_OR_NEWER
    public enum Space {
        World,
        Self,
    }
#endif

    [Serializable]
    [MemoryPackable]
    public partial class Transform : Component {
#if !UNITY_5_3_OR_NEWER // 在 unity 里, 就用 unity 的api, 要不然会白白浪费 unity 的计算.
        [MemoryPackInclude]
        private Vector3 _localPosition;

        [MemoryPackInclude]
        private Quaternion _localRotation = Quaternion.Identity;

        [MemoryPackInclude]
        private Vector3 _localScale = Vector3.One;

        private Matrix4x4 _localMatrix = Matrix4x4.Identity;
        private Matrix4x4 _worldMatrix = Matrix4x4.Identity;

        private bool _dirty = true;
        private bool _dirtyRotation = true; // 这两个计算耗时, 所以用脏数据处理
        private bool _dirtyScale = true;
        private Quaternion _rotation;
        private Vector3 _scale;

        [MemoryPackIgnore]
        public Matrix4x4 LocalMatrix {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => this._localMatrix;
        }

        [MemoryPackIgnore]
        private ref Matrix4x4 WorldMatrixRef {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get {
                this.UpdateWorldMatrix();
                return ref this._worldMatrix;
            }
        }

        [MemoryPackIgnore]
        public Matrix4x4 WorldMatrix {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get {
                this.UpdateWorldMatrix();
                return this._worldMatrix;
            }
        }

        [MemoryPackIgnore]
        public Vector3 Position {
            get {
                this.UpdateWorldMatrix();
                return this._worldMatrix.position;
            }
            set {
                var pt = this.GetParentTransform();
                if (pt == null) {
                    this._localPosition = value;
                }
                else {
                    Matrix4x4.Invert(ref pt.WorldMatrixRef, out var invParent);
                    Matrix4x4.TransformPoint(ref invParent, ref value, out this._localPosition);
                }

                MarkDirty(this.Entity);
            }
        }

        [MemoryPackIgnore]
        public Vector3 LocalPosition {
            get => this._localPosition;
            set {
                this._localPosition = value;
                MarkDirty(this.Entity);
            }
        }

        [MemoryPackIgnore]
        public Quaternion Rotation {
            get {
                this.UpdateWorldMatrix();
                if (this._dirtyRotation) {
                    this._rotation = this._worldMatrix.rotation;
                    this._dirtyRotation = false;
                }

                return this._rotation;
            }
            set {
                var pt = this.GetParentTransform();
                if (pt == null) {
                    this._localRotation = value;
                }
                else {
                    var invParent = Quaternion.Inverse(pt.Rotation);
                    this._localRotation = invParent * value;
                }

                MarkDirty(this.Entity);
            }
        }

        [MemoryPackIgnore]
        public Quaternion LocalRotation {
            get => this._localRotation;
            set {
                this._localRotation = value;
                MarkDirty(this.Entity);
            }
        }

        [MemoryPackIgnore]
        public Vector3 Scale {
            get {
                this.UpdateWorldMatrix();
                if (this._dirtyScale) {
                    this._scale = this._worldMatrix.scale;
                    this._dirtyScale = false;
                }

                return this._scale;
            }
        }

        [MemoryPackIgnore]
        public Vector3 LocalScale {
            get => this._localScale;
            set {
                this._localScale = value;
                MarkDirty(this.Entity);
            }
        }

        [MemoryPackIgnore]
        public Vector3 EulerAngles {
            get => this.Rotation.eulerAngles;
            set => this.Rotation = Quaternion.Euler(value);
        }

        [MemoryPackIgnore]
        public Vector3 LocalEulerAngles {
            get => this.LocalRotation.eulerAngles;
            set => this.LocalRotation = Quaternion.Euler(value);
        }

        [MemoryPackIgnore]
        public Vector3 Forward {
            get {
                this.UpdateWorldMatrix();
                return this._worldMatrix.forward;
            }
            set => this.Rotation = Quaternion.CreateLookRotation(value);
        }

        [MemoryPackIgnore]
        public Vector3 Right {
            get {
                this.UpdateWorldMatrix();
                return this._worldMatrix.right;
            }
            set => this.Rotation = Quaternion.CreateFromToRotation(Vector3.Right, value);
        }

        [MemoryPackIgnore]
        public Vector3 Up {
            get {
                this.UpdateWorldMatrix();
                return this._worldMatrix.right;
            }
            set => this.Rotation = Quaternion.CreateFromToRotation(Vector3.Up, value);
        }

        public void LookAt(Vector3 position) {
            this.LookAt(position, Vector3.Up);
        }

        public void LookAt(Vector3 position, Vector3 worldUp) {
            var forward = position - this.Position;
            this.Rotation = Quaternion.CreateLookRotation(forward, worldUp);
        }

        private Transform GetParentTransform() {
            var parent = this.Parent;
            while (parent != null) {
                var pt = parent.transform;
                if (pt != null)
                    return pt;

                parent = parent.Parent;
            }

            return null;
        }

        private void UpdateWorldMatrix() {
            if (!this._dirty)
                return;

            this._localMatrix = Matrix4x4.CreateTRS(this._localPosition, this._localRotation, this._localScale);

            var pt = this.GetParentTransform();
            this._worldMatrix = pt != null ? pt.WorldMatrixRef * this._localMatrix : this._localMatrix;

            this._dirty = false;

            var e = this.Entity;
            if (e.children != null) {
                for (int i = 0, len = e.children.Count; i < len; i++) {
                    var child = e.children[i];
                    MarkDirty(child);
                }
            }
        }

        private static void MarkDirty(Entity e) {
            e.transform?.Dirty();

            if (e.children == null)
                return;

            for (int i = 0, len = e.children.Count; i < len; i++) {
                var child = e.children[i];
                MarkDirty(child);
            }
        }

        private void Dirty() {
            this._dirty = true;
            this._dirtyRotation = true;
            this._dirtyScale = true;
        }

        internal override void OnParentChangedInternal(Entity previousParent, int layer) {
            this.Dirty();
        }
        
        public void Translate(Vector3 translation, Space relativeTo) {
            if (relativeTo == Space.Self) {
                Matrix4x4.TransformDirection(ref this.WorldMatrixRef, ref translation, out translation);
            }

            this.Position += translation;
        }

        public void Translate(Vector3 translation) {
            this.Translate(translation, Space.Self);
        }
        
        public void Rotate(Vector3 eulers, Space relativeTo) {
            var quat = Quaternion.Euler(eulers);
            if (relativeTo == Space.Self) {
                this.LocalRotation *= quat;
            }
            else {
                this.Rotation *= Quaternion.Inverse(this.Rotation) * quat * this.Rotation;
            }
        }

        public void Rotate(Vector3 eulers) {
            this.Rotate(eulers, Space.Self);
        }

        public void Rotate(Vector3 axis, float angle, Space relativeTo) {
            if (relativeTo == Space.World) {
                var invRotation = Quaternion.Inverse(this.Rotation);
                axis = invRotation * axis;
            }

            var rotation = Quaternion.CreateAngleAxis(axis, angle);
            this.Rotation *= rotation;
        }

        public void Rotate(Vector3 axis, float angle) {
            this.Rotate(axis, angle, Space.Self);
        }

        public void RotateAround(Vector3 point, Vector3 axis, float angle) {
            var rotation = Quaternion.CreateAngleAxis(axis, angle);
            var direction = this.Position - point;
            direction = rotation * direction;
            this.Position = point + direction;
            this.Rotation = rotation * this.Rotation;
        }

#endif

        public void NormalizeValue() {
            this.LocalPosition = Vector3.Zero;
            this.LocalRotation = Quaternion.Identity;
            this.LocalScale = Vector3.One;
        }

        public void LookRotationLerp(Vector3 forward, float t) {
            var forwardRotation = Quaternion.CreateLookRotation(forward, Vector3.Up);
            this.Rotation = Quaternion.Lerp(this.Rotation, forwardRotation, t);
        }

        public void LookRotation(Vector3 forward) {
            var forwardRotation = Quaternion.CreateLookRotation(forward, Vector3.Up);
            this.Rotation = forwardRotation;
        }
    }
}